Skip to content

Partial expression evaluation, example of a builtin function #1115

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Jun 5, 2025

Conversation

andrzej-krupka
Copy link
Contributor

No description provided.

@andrzej-krupka andrzej-krupka force-pushed the andrzej/partial-expression-evaluation branch 2 times, most recently from 0c0de3a to e9420d9 Compare June 2, 2025 21:09
def evaluate(
self,
context: EvaluationContext,
raise_error: bool = True,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strict has been renamed to raise_error, feels more verbose.

force_evaluate basically means that we don't evaluate blacklisted symbols at all, thus yielding "partial evaluation" that we needed.

As an example:

from flow360 import control, math

x = UserVariable(name="x", value = 1)

# Works with python
model.expression = math.cross([x, 2 * x, 1], control.machRef)

# Also works with strings...
model.expression = "math.cross([x, 2 * x, 1], control.machRef)"

# When converting to solver, cross is inlined:
# [
#     (2 * x) * ((solution.coordinate)[2]) - (1 * ((solution.coordinate)[1])),
#     1 * ((solution.coordinate)[0]) - (x * ((solution.coordinate)[2])),
#     x * ((solution.coordinate)[1]) - ((2 * x) * ((solution.coordinate)[0]))
# ]

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I propose raise_error -> raise_on_non_evaluable
force_evaluate -> evaluate_numerically

something like these.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First one I agree with. For the second one I stand by force_evaluate, because the flag changes the behavior on handling symbols that return False for can_evaluate (solver variables in our case), essentially forcing those symbols to be evaluated to a numeric value. I think evaluate_numerically would fit if we changed evaluable and can_evaluate to something like numeric and has_numeric_value respectively.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed raise_error to raise_on_non_evaluable

pattern = f"({'|'.join(escaped_delimiters)})"
result = re.split(pattern, value)
return [part for part in result if part != ""]
def __soft_fail_sub__(self, other):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a port of #1092

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we overloading enough operators?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to overload all binary operators that have a left and right version. I'll double check and write a test for it.

@andrzej-krupka andrzej-krupka force-pushed the andrzej/partial-expression-evaluation branch from e9420d9 to 5e7e746 Compare June 2, 2025 21:11
@benflexcompute
Copy link
Collaborator

There are cyclic pylint errors. I have not got chance to take a look.

@andrzej-krupka
Copy link
Contributor Author

andrzej-krupka commented Jun 3, 2025

There are cyclic pylint errors. I have not got chance to take a look.

Yup, in context.py there are cyclic imports, but I believe in this specific case they're needed. Here's my reasoning:

  1. When the user_code module is imported the use can request the module to evaluate an expression. When he does that, the expression body (a string) gets parsed into an AST and the nodes are evaluated. For name nodes we need to resolve those string names to valid python objects. We expose multiple modules (for example units, variables, math functions and possibly more to come in the future...) and we cannot guarantee they are imported at the time of evaluation. This is why we need to dynamically import them as we evaluate the expression at runtime.
  2. To my understanding of Python circular imports only cause errors when we try to import modules that are not initialized fully (so usually for top-level imports). Since expression string evaluation only happens after the modules are fully initialized and the circular imports are not top-level we should not run into issues (solver variables which are initialized along with the modules are a bit tricky but since unit system imports are not circular its not an issue, I think we could run into issues if we, for example, used a math function within a solver variable value definition which doesn't happen.

if len(expr.elements) == 1:
return f"{{{elements[0]}}}"
return f"{{{', '.join(elements)}}}"
return "std::vector<float>()"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a bit confused. What is the use case of this? What expression would trigger this line?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe (,) would being an empty tuple declaration. Considering we are not using it perhaps would be better to just disallow it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK changed to error for stricter checking

@@ -158,8 +157,8 @@ def _list(expr, syntax, name_translator):
return f"[{elements_str}]"
if syntax == TargetSyntax.CPP:
if len(expr.elements) == 0:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. Should we throw exception in this case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the general case its still a valid python syntax so it makes some sense to have this. It will just fail in the validator for any DimensionedType so technically it is never reached for our use cases.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK changed to error for stricter checking


force_evaluate (bool): If True, evaluate evaluable objects marked as
non-evaluable, instead of returning their identifier.
inline (bool): If True, inline certain marked function calls.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still need this line?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops, nope. It's a leftover. Will remove.

@andrzej-krupka andrzej-krupka force-pushed the andrzej/partial-expression-evaluation branch from 145584b to 00f639c Compare June 5, 2025 14:32
@andrzej-krupka andrzej-krupka force-pushed the andrzej/partial-expression-evaluation branch from 00f639c to 1c67711 Compare June 5, 2025 14:48
@@ -158,8 +157,8 @@ def _list(expr, syntax, name_translator):
return f"[{elements_str}]"
if syntax == TargetSyntax.CPP:
if len(expr.elements) == 0:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK changed to error for stricter checking

if len(expr.elements) == 1:
return f"{{{elements[0]}}}"
return f"{{{', '.join(elements)}}}"
return "std::vector<float>()"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK changed to error for stricter checking

@benflexcompute benflexcompute enabled auto-merge (squash) June 5, 2025 15:11
@benflexcompute benflexcompute merged commit 61beaf5 into expressions Jun 5, 2025
3 checks passed
@benflexcompute benflexcompute deleted the andrzej/partial-expression-evaluation branch June 5, 2025 15:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants